/*
 * ============================================================================
 *                   GNU Lesser General Public License
 * ============================================================================
 *
 *
 *
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
 * 
 *
 *
 */
package rad.framework.security.jpa;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.security.auth.Subject;

import rad.framework.el.Context;
import rad.framework.el.ExpressionUtil;
import rad.framework.log.Logger;
import rad.framework.security.JAASUtil;
import rad.framework.security.jpa.Action;
import rad.framework.security.jpa.Permissions;
import rad.framework.security.jpa.PrincipalsAllowed;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This class contains all the logic to check entity permissions.
 * 
 * @author boudyacho
 * 
 */
public class PermissionsUtil {

	private static final Log logger = LogFactory.getLog(PermissionsUtil.class);

	public static String getFilter(Class<?> entityClass, String alias) {
		return getFilter(JAASUtil.getSubject(), JAASUtil.getPrincipal(), entityClass, alias);
	}
	
	public static String getFilter(Subject subject, Principal caller, Class<?> entityClass, String alias) {
		Permissions permissions = entityClass.getAnnotation(Permissions.class);
		// no rights defined so ok
		if (permissions == null) {
			return null;
		}

		// gather the security info
		Logger.debug(logger, "Subject: {0}", subject);
		List<Principal> roles = JAASUtil.getRoles(subject);

		if (hasRole(permissions.value(), roles, Action.READ)) {
			return null;
		}

		StringBuffer buffer = new StringBuffer();

		for (PrincipalsAllowed permission : permissions.value()) {
			if (hasAction(permission, Action.READ)) {
				// need to have one of these
				String[] principals = permission.roleExpressions();
				for (int i = 0; i < principals.length; i++) {
					if (buffer.length() > 0) {
						buffer.append(" or ");
					}
					buffer.append(alias);
					buffer.append(".");
					buffer.append(principals[i]);
					buffer.append(" in (");
					buffer.append(getPrincipalNames(roles));
					buffer.append(")");
				}

				principals = permission.userExpressions();
				for (int i = 0; i < principals.length; i++) {
					if (buffer.length() > 0) {
						buffer.append(" or ");
					}
					buffer.append(alias);
					buffer.append(".");
					buffer.append(principals[i]);
					buffer.append(" = '");
					buffer.append(caller.getName());
					buffer.append("'");
				}
			}
		}

		if (buffer.length() == 0) {
			return null;
		} else {
			return "(" + buffer.toString() + ")";
		}
	}

	public static String getRoleFilter(Class<?> entityClass, String alias) {
		return getRoleFilter(JAASUtil.getSubject(), entityClass, alias);
	}
	
	public static String getRoleFilter(Subject subject, Class<?> entityClass, String alias) {
		Permissions permissions = entityClass.getAnnotation(Permissions.class);
		// no rights defined so ok
		if (permissions == null) {
			return null;
		}

		// gather the security info
		Logger.debug(logger, "Subject: {0}", subject);
		List<Principal> roles = JAASUtil.getRoles(subject);

		if (hasRole(permissions.value(), roles, Action.READ)) {
			return null;
		}

		StringBuffer buffer = new StringBuffer();

		for (PrincipalsAllowed permission : permissions.value()) {
			if (hasAction(permission, Action.READ)) {
				// need to have one of these
				String[] principals = permission.roleExpressions();
				for (int i = 0; i < principals.length; i++) {
					if (buffer.length() > 0) {
						buffer.append(" or ");
					}
					buffer.append(alias);
					buffer.append(".");
					buffer.append(principals[i]);
					buffer.append(" in (");
					buffer.append(getPrincipalNames(roles));
					buffer.append(")");
				}
			}
		}

		if (buffer.length() == 0) {
			return null;
		} else {
			return buffer.toString();
		}
	}

	public static String getUserFilter(Class<?> entityClass, String alias) {
		return getUserFilter(JAASUtil.getSubject(), JAASUtil.getPrincipal(), entityClass, alias);
	}
	
	public static String getUserFilter(Subject subject, Principal caller, Class<?> entityClass, String alias) {
		Permissions permissions = entityClass.getAnnotation(Permissions.class);
		// no rights defined so ok
		if (permissions == null) {
			return null;
		}

		// gather the security info
		Logger.debug(logger, "Subject: {0}", subject);

		StringBuffer buffer = new StringBuffer();

		for (PrincipalsAllowed permission : permissions.value()) {
			if (hasAction(permission, Action.READ)) {
				// need to have one of these
				String[] principals = permission.userExpressions();
				for (int i = 0; i < principals.length; i++) {
					if (buffer.length() > 0) {
						buffer.append(" or ");
					}
					buffer.append(alias);
					buffer.append(".");
					buffer.append(principals[i]);
					buffer.append(" = '");
					buffer.append(caller.getName());
					buffer.append("'");
				}
			}
		}

		if (buffer.length() == 0) {
			return null;
		} else {
			return buffer.toString();
		}
	}

	private static String getPrincipalNames(List<Principal> roles) {
		StringBuffer b = new StringBuffer();
		for (Principal principal : roles) {
			if (b.length() > 0) {
				b.append(", ");
			}
			b.append("'");
			b.append(principal.getName());
			b.append("'");
		}
		return b.toString().trim();
	}

	/**
	 * Filter out objects based on READ permissions.
	 * 
	 * @param output
	 * @return
	 */
	public static Object filter(Object output) {
		return filter(JAASUtil.getSubject(), JAASUtil.getPrincipal(), output);
	}
	
	public static Object filter(Subject subject, Principal caller, Object output) {
		// no need to do anything if the output is null or empty
		if (output == null) {
			return output;
		}
		List list = null;
		if (output instanceof List) {
			list = (List) output;
			if (list.size() == 0) {
				return output;
			}
		} else if (output.getClass().isPrimitive()) {
			return output;
		}

		// gather the security info
		Logger.debug(logger, "Subject: {0}", subject);
		List<Principal> roles = JAASUtil.getRoles(subject);

		if (list == null) {
			// handle single result
			if (hasPermissions(roles, caller, output, Action.READ)) {
				Logger.debug(logger, "Granted: {0}", output);
				return output;
			} else {
				throw new SecurityException("Access Denied: " + output);
			}
		} else {
			// handle query result
			List<Object> filtered = new ArrayList<Object>();
			for (Iterator i = list.iterator(); i.hasNext();) {
				Object element = (Object) i.next();

				if (hasPermissions(roles, caller, element, Action.READ)) {
					Logger.debug(logger, "Granted: {0}", element);
					filtered.add(element);
				} else {
					Logger.debug(logger, "Denied: {0}", element);
				}
			}
			return filtered;
		}
	}

	/**
	 * Check INSERT, UPDATE, or REMOVE permissions.
	 * 
	 * @param entity
	 * @param action
	 */
	public static void doCheck(Object entity, Action action) {
		Permissions permissions = entity.getClass().getAnnotation(
				Permissions.class);
		if (permissions == null) {
			return;
		}
		List<Principal> roles = JAASUtil.getRoles();
		Principal caller = JAASUtil.getPrincipal();
		if (!hasPermissions(roles, caller, entity, action)) {
			throw new SecurityException(action + " Denied for " + entity);
		}
	}

	public static boolean hasPermissions(Class clazz, Action action) {
		Permissions permissions = (Permissions) clazz.getAnnotation(Permissions.class);
		if (permissions == null) {
			return true;
		}
		PrincipalsAllowed[] allowed = permissions.value();
		List<Principal> roles = JAASUtil.getRoles();
		return hasRole(allowed, roles, action);
	}

	public static boolean hasPermissions(Object entity, Action action) {
		Permissions permissions = entity.getClass().getAnnotation(
				Permissions.class);
		if (permissions == null) {
			return true;
		}
		List<Principal> roles = JAASUtil.getRoles();
		Principal caller = JAASUtil.getPrincipal();
		return hasPermissions(roles, caller, entity, action);
	}
	
	/**
	 * Check if the user has role based or instance based permission.
	 * 
	 * @param roles
	 * @param caller
	 * @param entity
	 * @param action
	 * @return
	 */
	protected static boolean hasPermissions(List<Principal> roles,
			Principal caller, Object entity, Action action) {
		Permissions permissions = entity.getClass().getAnnotation(
				Permissions.class);
		// no rights defined so ok
		if (permissions == null) {
			return true;
		}

		PrincipalsAllowed[] allowed = permissions.value();
		return hasRole(allowed, roles, action)
				|| hasInstancePermission(allowed, roles, caller, entity, action);
	}

	/**
	 * Check to see if at least one of the user's roles is granted the action.
	 * 
	 * @param allowed
	 * @param roles
	 * @param desiredAction
	 * @return
	 */
	protected static boolean hasRole(PrincipalsAllowed[] allowed,
			List<Principal> roles, Action desiredAction) {
		for (int i = 0; i < allowed.length; i++) {
			if (hasAction(allowed[i], desiredAction)
					&& hasPrincipal(roles, allowed[i].roles())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check to see if the user matched at least one of the expressions granted
	 * the action.
	 * 
	 * @param allowed
	 * @param roles
	 * @param caller
	 * @param o
	 * @param action
	 * @return
	 */
	protected static boolean hasInstancePermission(PrincipalsAllowed[] allowed,
			List<Principal> roles, Principal caller, Object o, Action action) {
		roles.add(caller);
		for (int i = 0; i < allowed.length; i++) {
			if (hasAction(allowed[i], action)
					&& hasPrincipal(roles, getPrincipalNames(o, allowed[i]
							.userExpressions()))) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check if the desired action is specified.
	 * 
	 * @param allowed
	 * @param desiredAction
	 * @return
	 */
	protected static boolean hasAction(PrincipalsAllowed allowed,
			Action desiredAction) {
		for (int i = 0; i < allowed.actions().length; i++) {
			if (allowed.actions()[i].equals(desiredAction)
					|| allowed.actions()[i].equals(Action.ALL)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check that at least one of the principals matches a specified names.
	 * 
	 * @param principals
	 * @param names
	 * @param action
	 * @return
	 */
	protected static boolean hasPrincipal(List<Principal> principals,
			String[] names) {
		for (Iterator i = principals.iterator(); i.hasNext();) {
			Principal p = (Principal) i.next();
			String name = p.getName();
			for (int j = 0; j < names.length; j++) {
				if (name.equals(names[j])) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Get the principal names for the expressions.
	 * 
	 * @param o
	 * @param expressions
	 * @return
	 */
	private static String[] getPrincipalNames(Object o, String[] expressions) {
		String[] names = new String[expressions.length];
		for (int i = 0; i < expressions.length; i++) {
			names[i] = (String) getPrincipalName(o, expressions[i]);
		}
		return names;
	}

	/**
	 * Get the principal name for the expression.
	 * 
	 * @param o
	 * @param el
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected static Object getPrincipalName(Object o, String el) {
		Context context = new Context();
		context.put("entity", o);
		return ExpressionUtil.getValue(context, "#{entity." + el + "}");
	}
}